C++ EXCEPTION HANDLING

Copyright Kent Sandvik 1995-1996

This article has been edited and published in the October edition 1995 of MacTech Journal.

C++ exception handling is a new feature that has been recently added to the Metrowerks C++ environment (and soon available with the Symantec C++ environment as well). Earlier various frameworks provided macros and various capabilities that emulated to some degree exception handling including emulating the syntax and semantics defined in the Annotated C++ Reference manual (MacApp, TCL, PowerPlant) using setjmp/longjmp and similar techniques. The Annotated C++ Reference Manual, Ellis & Stroustrup, is by the way the reference that C++ programmers should use, and the future ANSI C++ reference will most likely take over the role as the rule book for portable C++ code.

C++ exception handling has not been widely available to Macintosh programmers until now. Most of the C++ literature dealing with exception handling tend to be more academical in nature versus showing how exception handling could be used in real life programming exercises. This article will attempt to provide simple examples to get you started with using exception handling in C++ programming work for Macintosh applications.

ERROR HANDLING AND EXCEPTIONS

The idea with exception handling is to provide a standard method of dealing with exceptional conditions on a higher level. Examples of exceptional conditions are: running out of memory or disk space, can't open a file that was assumed to be OK to open, math errors, parsing errors of data that is assumed to be correct and so on. Note that the word exception is important. Exception handling should not be used as a nice way to signal information from one level to another, neither should it be used as an assertion system to find out about every possible error encountered.

Two unrelated parts of the application environment will signal to each other about unexpected events. For instance the user interface side of an application wants that certain C++ objects are constructed, let's say that a network connection is needed. This part of the code will try to create such objects, if the object creation leads to an unexpected state (suddenly can't make the connection), the network object could throw an exception back to the code that tries to use it.

It it important to note that exception handling is just one of many ways how to deal with error cases. We have the traditional software engineering concepts of:

Examples of error situations are truly fatal errors, precondition/invariant failures (we assume that something is OK, and it is not), algorithmic failures (like the dreadful one-off with arrays), and alternate return signals (end of file markers arriving with a file stream for instance).

The issue is if exception handling should take over the role of all these cases, or be used selectively. One approach is to indeed reserve exceptions for exceptional cases. Another extreme is to use exceptions for all error situations. This is where more practical experience by using exceptions will show what would make sense, and what is overkill. We also have to factor in the issues of performance reliability of initial compiler versions supporting exception handling, as well as design issues concerning reuse of class frameworks.

Issues that speaks in favor or exception handling is that the coding design is cleaner if the error handling is factored out to specific, reusable catch blocks. In other words reuse of error handling increases, and the actual function blocks would spend less time dealing with error situations, instead delegate this to specifically designed catch blocks that will deal uniformly with error situations. For instance such catch blocks would provide a uniform way of informing end users about error conditions.

In addition such code could be reused with various projects. From a design point of view separating error-case from normal-case will make it easier to focus separately on each case. In other words if something works fine 99% of the cases, then the normal-case code should be optimized for this. And the exception case should handle completely the 1% of error cases.

EXCEPTION BASICS

The basic idea with an exception is that someone somewhere will signal that they are in trouble. On a higher level an exception handler is triggered, and depending on the case various actions could take place. It is up to the programmer to define what part of a code should be marked concerning where exceptions might happen. In other words:

       DoSomething()
       {
               // I'm in trouble
               throw exception;
       }


       try {
               // let's see what happens
               DoSomething();
       };

       catch (exception)
       {
               // Ooops, what to do?
       }

So what should we do inside the catch block. We might change something and retry the action, return normally (as nothing really happened), rethrow the same exception, maybe with modifications and assume that a higher level catch block will help out, throw a different exception, continue execution to the next statement, or just terminate the program.

When we design applications it is important to define what each possible layer of the application (from the low level utility functions all the way to the end user level) should take what action with an exception when such exceptions are triggered. Various libraries, such as the new ANSI C++ standard library, does include exceptions thrown from various class member functions. It is important that the application will catch any relevant exceptions thrown -- assuming that the exceptions are indeed documented.

As the compiler will handle the exception handling based on a standard syntax and semantics, this means that such code is highly portable across compiler environments and platforms. There are of course gotchas, for instance the Macintosh programs should not assume too much about infinite stack space unlike UNIX systems. There's also a performance penalty when using exception handling. Note that C++ exception handling ensures that all class instances will be properly de-allocated on the runtime stack. This means that we could not really make use of constructors and destructors that do a lot of work, and if these fail, the de-allocation code is prepared by the compiler so that we don't need to worry about memory leaks and non-allocated objects on the stack. For instance a constructor could try create files, and if this fails, the destructor will remove any temporary files. All this is triggered when the possible objects unwind via the stack, after an exception has happened. In other words constructors and destructors could are now far more productive.

Needless to say, C++ exception handling needs runtime support. In the case of normal functions the compiler knows the point of call which function will actually be called. In the case of exception handling the compiler does not know for a particular throw expression what function the catch block resides in, and where the execution will resume after the exception has been handled. These decisions won't happen until runtime. This means that the compiler will leave information around for the proper decisions to take place (generates data structures to hold this information needed during runtime). This means additional overhead in the application execution, stack and so on.

EXAMPLE OF C++ EXCEPTION CODING, TRY AND CATCH

Let's investigate each case one at a time looking at real C++ code.

       try 
       {
       aFoo = new Foo;
       
       aFoo->DoASillyThing();
       
       aBar = new Bar;
       }

Here we will do an initialization of a Foo class, and then call a member function that will in our case trigger an unexpected situation (that was hard coded). The try block will tell the compiler that it should catch any exceptions happening from this exercise.

       catch (...) // catch everything
       {
               cout <<"We catch every single exception here.\n";
	}
Here's the catch block that is usually programmed after the try case. In this first example we will try to catch every possible exception that is bubbling up from the try case (note the ... notation).

However, in most cases it makes sense to specialize on the exceptions that we want to catch:

       catch (TSeriousMacException &ex)
       {
               cout << "SERIOUS EXCEPTION: " << ex.GetExceptionMessage() << ",  OSErr: " << 				ex.GetExceptionOSErr() <<
			", File: " << ex.GetExceptionFile() << ",  Line: " << ex.GetExceptionLine() << 				endl;

		// Use real Mac UI to signal about the seriousness to the user of the application.
		Delay(3*60, NULL);
		ExitToShell();
	
	}
	
	catch(TMacException &ex)
	{
		cout << "EXCEPTION: " << ex.GetExceptionMessage() << ",  OSErr: " << 					ex.GetExceptionOSErr() <<
		", File: " << ex.GetExceptionFile() << ", Line: " << ex.GetExceptionLine() << endl;
	}

 	// After the exception is handled, we will continue here...
In this case we catch specific exceptions. Note that these exceptions are actually objects. Furthermore exceptions could be strings ("Help"), integers and various other data structures. In most cases it makes sense to build an exception class hierarchy (more about this later). You should also note that the exceptions are caught from the specific (sub-class) to the generic. This ordering is important, if you catch a parent class exception before the child one, the child exception is never handled. In this case the TSeriousMacException is handled first, even if itÕs inherited from the TMacException class. Note also that we will terminate the application with ExitToShell when the TSeriousMacException is caught, otherwise we will continue if the TMacException is handled.

Note that the execution of the code code, after the exception in this particular catch block, will continue on the next code line. The execution of the program does not resume where the exception was thrown. However, if the catch block wants to rethrow the exception, as in:

       catch(TMacException &ex)
       {
               throw;
       }

Or otherwise modify the current exception object, or throw another exception object, then the next layer or catch frames will take over (assuming these layers exist). Note also that there will not be a recursion if the same exception object is thrown again, in other words the same exception block is not triggered again.

As shown in the example, the exception objects could contain both member functions and fields. This is handy; when we then throw the exception we could provide information back to the catch block about the situation. As we pass the exception object as a reference, we could modify the object, and pass new information along if we want to rethrow the same exception.

EXCEPTION CLASSES

In this case we have defined the exception classes as:

class TMacException 
{
public:
       TMacException(const char *theMessage, const OSErr theError) :
                                       fMessage(theMessage), 
                                       fError(theError), 
                                       fFileName("NO FILE SPECIFIED"), 
                                       fLineNumber(0L) 
                                       {};
                                       
       TMacException(const char *theMessage, const OSErr theError, const char *theFileName, const                      long theLineNumber) :
                                       fMessage(theMessage), 
                                       fError(theError), 
                                       fFileName(theFileName), 
                                       fLineNumber(theLineNumber) 
                                       {};
                                       
       const char*     GetExceptionMessage(void)       { return fMessage;};
       const OSErr     GetExceptionOSErr(void) { return fError;};
       const char*     GetExceptionFile(void)          { return fFileName;};
       const long      GetExceptionLine(void)          { return fLineNumber;};
       
protected:
       const OSErr     fError;
       const char*     fMessage;
       const char*     fFileName;
       const long      fLineNumber;
};


class TSeriousMacException : public TMacException
{
public:
       TSeriousMacException(const char *theMessage, const OSErr theError) :
                                       TMacException(theMessage, theError, "NO FILE SPECIFIED", 0L)
                                       {};
                                       
       TSeriousMacException(const char *theMessage, const OSErr theError, const char                                                   *theFileName, const long theLineNumber) :
                       TMacException(theMessage, theError, theFileName, theLineNumber)
                                       {};

};

The TMacException is the base class. This class has fields for storing the OSErr, a string message, file name, line number and it could also contain other various fields. We place these fields into a protected area so this is the reason we need inlined Get functions for these (this is not a requirement, but when we do object oriented design, data encapsulation is a good thing to do anyway).

TSeriousMacException is an interesting class, as it does not really contain any additional fields or member functions. Instead we pass the values back to the TMacException when we construct the exception class. Why? Well, this was a way to signal that this class is more serious than the normal TMacException, and when we look back at the catch block that catches from specific to more generic exceptions we now have a way to signal the catch block the seriousness of the exception.

THROWING EXCEPTIONS

So far we have shown both the try and catch blocks, but what about throwing exceptions? Here's finally the DoASillyThing member function:

void Foo::DoASillyThing(void)
{
       throw TMacException("We did a silly thing", unimpErr, __FILE__, __LINE__);
}

or:

throw TSeriousMacException("We did a really serious, silly thing", unimpErr, __FILE__, __LINE__);

In other words, what we do is to create an exception object as part of the throw action. We could also do things like:

throw "Help me!";

or

throw anOSErr;

and then we assume that there's a catch block that will indeed catch such exceptions that will catch strings or OSErr values (or catches any exception). In this specific case I wanted to write a flexible exception class that contains as much information about the situation as possible, including possible OSErr values and an information string. The __FILE__ and __LINE__ macros for providing information where the exception was triggered also helps out when debugging C++ code. If typing these variables makes your fingers bleed, here's a macro that helps out:

#define THROWEXCEPTION(name, number) \
       throw TMacException( (name), (number), __FILE__, __LINE__)
       
#define THROWSERIOUSEXCEPTION(name, number) \
       throw TSeriousMacException( (name), (number), __FILE__, __LINE__)

Note that the object is constructed at the throw point. This might sometimes not look at the case, as in:

enum MyFailures { noErr, bigErr, semiBigErr};
enum myState = noErr;

{
       throw myState;   // we construct the exception object here, not before!
}

One problem with constructors and destructors in C++ is that they don't return any values, so usually you need to implement state information fields inside the class and poll these to know what happened after a constructor or destructor was triggered. Exception handling will now help out, as you could throw exceptions from a constructor or destructor, or:

Bar::Bar()
{
       throw TMacException("Problems inside the Bar constructor", noErr, __FILE__, __LINE__);
}

Note that such exception handling should not be used as a way to signal state directly -- instead this is a way to signal back to the higher level of abstraction in the code that the constructor never completed fully, so that this instance of the class is not fully operational.

APPLICATION DESIGN

This leads to a discussion about design and exceptions. It is important to fully define how exceptions are triggered, and what parts of the code will intercept and possible rethrow the exceptions (using the rethrow key words from inside the catch block) to the next level. If this is not fully architectured, it might be very hard to know what was going on inside the code when exceptions are triggered. Here's an example of a simple three-level design using exceptions:

LOW LEVEL (this is where the functional code is operating)

Throw exceptions when exceptional cases happen

MIDDLE LEVEL

Handle exceptions and retry if this is doable, if not pass the exception forward

HIGH LEVEL (USER INTERFACE LEVEL)

Take care of all exceptions, back out if needed, worst case terminate the program, but provide the end user a chance to save modified data.

TERMINATION

By default when an exception is thrown, and no handlers exist for the thrown exception, the built-in function terminate is called. In the default behavior terminate will call abort (that terminates the program). It makes sense to override this function for more control of what is going on, especially if you are using an external C++ library that will throw both known exceptions (documented) and unknown exceptions (not documented). You could override the default terminate function by using the set_terminate function call (note the correspondence with set_new_handler), as in:

void HandleTerminate(void)
{
       cout << "This is the my own terminate function that I've installed!" << endl;
}

set_terminate(HandleTerminate);

FINAL WORDS

Provided is a smaller Metrowerks project and C++ code that shows how the various exception parts work together. Feel free to comment and uncomment code lines in order to learn what is happening.

There's more to the exception handling, reading the Annotated C++ Manual you should get more esoteric information you might want to use. I would also recommend to carefully read the README files of the compiler environments as not all the features are yet implemented (but this changes from release to release). This article was an introduction to basic use of exceptions, and by using these in frameworks and other code constructs we all might learn more about the practical and theoretical limitations and pitfalls with this otherwise practical construct. Currently there's little experience using C++ exceptions in real-world applications, and any further knowledge about how to design exception classes and use these should if possible be shared amongst C++ developers.



REFERENCES: